夹心攻击(Sandwich Attack)技术原理与防御策略
深度解析:夹心攻击(Sandwich Attack)技术原理与防御策略
夹心攻击(Sandwich Attack)作为去中心化交易所(DEX)场景下的一种典型MEV(Miner Extractable Value)攻击方式,近年来受到广泛关注。本文将从技术原理、实现机制、攻击收益来源、防御策略等多个维度进行深度解析,并给出审计实战中的风险识别方法。
一、夹心攻击的核心定义与关系澄清
1.1 一句话定义
夹心攻击是一种典型的DEX场景下的MEV攻击,攻击者通过抢跑(Front-running)和尾随(Back-running)两笔交易,将用户的交易“夹在中间”,从而将用户的滑点损失转化为自己的套利利润。
1.2 关键概念澄清
| 概念 | 是否完整描述夹心攻击 | 核心含义 |
|---|---|---|
| Front-running | ❌ 不完整 | 指“抢在前面执行”交易 |
| Back-running | ❌ 不完整 | 指“在后面执行”交易,吃结果 |
| Sandwich Attack | ✅ | Front + Victim + Back 组合攻击 |
| MEV | ✅(更大集合) | 区块生产者/机器人可提取的最大价值 |
关键结论:Sandwich Attack是MEV的一种具体实现方式。
二、攻击成立的前提条件
夹心攻击并非适用于所有交易场景,其成立依赖于以下条件:
2.1 DEX使用AMM模型
典型如Uniswap v2/v3、SushiSwap、PancakeSwap等,价格由池子储备决定,而非订单簿撮合。
2.2 用户交易特征
用户交易需满足以下条件之一:
- 交易金额相对池子深度较大
- 滑点设置过宽(
minAmountOut设置过低) - 使用公共mempool(交易可见)
这些特征是攻击者“确定能赚钱”的信号。
三、夹心攻击的完整机制拆解
3.1 Mempool:攻击的起点
用户交易进入公共内存池(Mempool)后,机器人实时监听关键信息,如swapExactTokensForTokens、amountIn、minAmountOut及池子当前储备,并判断是否可以通过“先买、再让用户买、最后卖”的方式无风险套利。
3.2 攻击的三步结构
Tx₁: Attacker Buy (Front-run)
Tx₂: Victim Buy (Victim)
Tx₃: Attacker Sell (Back-run)
3.3 每一步在AMM中的变化(以Uniswap v2模型为例)
初始状态
ReserveA = x
ReserveB = y
Price = y / x
🥪 Step 1:攻击者抢跑买入(抬价)
攻击者用TKA买入TKB,导致池子中A增加、B减少,B价格上涨。这一步的目的是人为制造更差的价格给受害者。
🥪 Step 2:受害者被迫高价成交
由于价格已被抬高,但minAmountOut仍然满足,受害者交易成功但成交价格极差。这一步制造了额外价格冲击和可被收割的滑点空间。
🥪 Step 3:攻击者砸盘套利
攻击者将Step 1买到的TKB卖回池子,导致B增加、A减少,价格部分回落。攻击者赚取的利润为受害者多付出的价格差减去手续费。
四、Python模拟:夹心攻击的教学级实现
4.1 模拟代码
def simulate_sandwich_attack(dex_contract, attacker, victim, amount_victim_in):
print(f"🕵️ 机器人发现受害者准备用 {amount_victim_in} TKA 换取 TKB")
# 初始储备
resA_0 = dex_contract.functions.reserveA().call()
resB_0 = dex_contract.functions.reserveB().call()
print(f"初始池子: A={resA_0}, B={resB_0}")
# --- Step 1: 抢跑 ---
attacker_in = 200 * 10**18
dex_contract.functions.swapAtoB(attacker_in).transact({'from': attacker})
# --- Step 2: 受害者成交 ---
dex_contract.functions.swapAtoB(amount_victim_in).transact({'from': victim})
# --- Step 3: 攻击者卖出 ---
attacker_b = token_b.functions.balanceOf(attacker).call()
token_a_before = token_a.functions.balanceOf(attacker).call()
dex_contract.functions.swapBtoA(attacker_b).transact({'from': attacker})
token_a_after = token_a.functions.balanceOf(attacker).call()
profit = token_a_after - token_a_before - attacker_in
print(f"💰 攻击者净利润: {profit}")
4.2 模拟隐含的重要假设
- 假设攻击者一定能夹成功:实际中存在同区块竞争和更高手续费的机器人。
- 忽略了手续费:实际利润公式应为
Profit ≈ Victim Slippage − 2 × Swap Fee − Gas。 - 忽略v3的流动性分段:v3中sandwich更复杂,但依然存在。
五、攻击收益的本质来源
夹心攻击并非“偷钱”,而是“重分配”。攻击者赚取的是用户因滑点容忍度过高而在AMM曲线上多付出的部分。换句话说,攻击者把“用户对价格不敏感”的部分变现了。
六、防御手段(从弱到强)
6.1 严格设置minAmountOut
require(amountOut >= minAmountOut, "SLIPPAGE_TOO_HIGH");
效果:夹心第一步抬价后,受害者交易revert,攻击者被迫高位接盘。这是唯一能在协议层强制保护用户的方式。
6.2 私有交易通道(Flashbots/MEV-Share)
交易不进公共mempool,攻击者无法看见用户交易。缺点包括用户体验复杂和中心化信任假设。
6.3 提高流动性深度(经济防御)
TVL增加,同样的攻击成本上升,套利空间下降。大池子sandwich少,小池子是重灾区。
七、延伸思考
7.1 高级主题
- Uniswap v3中的sandwich策略变化
- JIT Liquidity与MEV
- MEV-Boost/PBS对攻击格局的影响
- 为什么“完全消灭MEV”在理论上不可行
7.2 一句话总结
夹心攻击不是漏洞,而是AMM、公共内存池和用户滑点容忍共同产生的必然结果。
如何在代码中识别Sandwich Attack风险(审计实战版)
一、从「入口函数」开始:最危险的函数长什么样?
1.1 高危函数签名
swap(...)
swapExactTokensForTokens(...)
swapExactETHForTokens(...)
swapTokensForExactTokens(...)
尤其是:
function swapExactTokensForTokens(
uint amountIn,
uint amountOutMin,
address[] calldata path,
address to,
uint deadline
)
审计信号:
amountOutMin是否来自用户?- 是否允许为
0? - 是否被协议“兜底修改”?
二、第一类核心风险:滑点保护是否真实存在
2.1 完全没有滑点保护(极高危)
uint amountOut = getAmountOut(amountIn);
_transfer(to, amountOut);
结论:100% sandwich可行,攻击者可随意操纵价格。
2.2 滑点参数存在,但未使用(经典漏洞)
function swap(uint amountIn, uint minOut) external {
uint out = getAmountOut(amountIn);
_swap(out);
// ❌ minOut没有任何校验
}
结论:非常常见的新手DEX错误。
2.3 滑点保护存在,但允许极端值
require(amountOut >= minOut, "SLIPPAGE");
但minOut == 0允许或前端默认给一个极低值。
审计结论:合约没bug,但系统性sandwich高风险。
三、第二类风险:价格是否完全由可操纵状态决定
3.1 AMM价格公式是否直接依赖reserve?
price = reserveB / reserveA;
或:
amountOut = (amountIn * reserveOut) / (reserveIn + amountIn);
判断:如果reserve只受swap影响且swap可被插队,则是sandwich的必要条件。
3.2 使用即时价格,而非时间加权价格(TWAP)
uint price = getSpotPrice();
而不是:
uint price = getTWAP(30 minutes);
结论:spot price可被瞬时操纵。
四、第三类风险:交易是否“原子可预测”
Sandwich成立的另一个关键是攻击者能精确预测你的成交结果。
4.1 成交金额完全确定
amountOut = getAmountOut(amountIn);
无随机性、无延迟、无外部依赖。
结论:非常适合sandwich bot做精确模拟,攻击收益高度确定。
4.2 单区块内完成所有状态更新
swap();
updateReserve();
风险说明:同一区块内front + victim + back完全可组合。
五、第四类风险:交易是否暴露在公共mempool
5.1 未提供私有交易入口
- 没有Flashbots/MEV-Share
- 没有commit–reveal
- 没有batch auction
审计语言:
Protocol relies on public mempool execution, making it vulnerable to MEV-based sandwich attacks.
六、第五类风险:是否存在“隐式sandwich放大器”
这些不是必要条件,但会放大攻击收益。
6.1 自动帮用户兜底滑点(反模式)
if (amountOut < minOut) {
minOut = amountOut * 95 / 100;
}
结论:这是反MEV的反面教材。
6.2 路由器自动拆单/聚合
swap(A → B → C)
路径越长,sandwich表面积越大。
6.3 针对大额交易无特殊处理
if (amountIn > threshold) {
// ❌ 没有任何保护
}
七、审计时可直接用的「Sandwich风险检查表」
[ ] 是否存在swap/trade函数
[ ] 是否使用AMM即时价格
[ ] 是否依赖reserve状态定价
[ ] 是否有minAmountOut校验
[ ] minAmountOut是否允许为0
[ ] 是否使用spot price而非TWAP
[ ] 是否暴露在公共mempool
[ ] 是否对大额交易做特殊处理
[ ] 是否存在自动调整滑点逻辑
八、审计结论该怎么写
示例审计描述
The protocol’s swap mechanism relies on spot AMM pricing and user-configurable slippage parameters. As transactions are executed via the public mempool, they are susceptible to MEV-based sandwich attacks, especially for large trades with high slippage tolerance. This is an economic design risk rather than a correctness bug.